/*!
 * Create HTTP errors
 * https://github.com/jshttp/http-errors
 */
import { inherits } from 'node:util'
import statuses from './statuses'
import toIdentifier from '../common/toIdentifier'

/**
 * Create a new HTTP Error.
 *
 * @returns {Error}
 */
export default function createError() {
  // so much arity going on ~_~
  let err
  let msg
  let status = 500
  let props = {}
  for(let i = 0; i < arguments.length; i++) {
    const arg = arguments[i]
    const type = typeof arg
    if(type === 'object' && arg instanceof Error) {
      err = arg
      status = err.status || err.statusCode || status
    } else if(type === 'number' && i === 0) {
      status = arg
    } else if(type === 'string') {
      msg = arg
    } else if(type === 'object') {
      props = arg
    } else {
      throw new TypeError('argument #' + (i + 1) + ' unsupported type ' + type)
    }
  }

  if(typeof status === 'number' && (status < 400 || status >= 600)) {
    console.error('non-error status code; use only 4xx or 5xx status codes')
  }

  if(typeof status !== 'number' ||
    (!statuses.message[status] && (status < 400 || status >= 600))) {
    status = 500
  }

  // constructor
  const HttpError = createError[status] || createError[codeClass(status)]

  if(!err) {
    // create error
    err = HttpError
      ? new HttpError(msg)
      : new Error(msg || statuses.message[status])
    Error.captureStackTrace(err, createError)
  }

  if(!HttpError || !(err instanceof HttpError) || err.status !== status) {
    // add properties to generic error
    err.expose = status < 500
    err.status = err.statusCode = status
  }

  for(let key in props) {
    if(key !== 'status' && key !== 'statusCode') {
      err[key] = props[key]
    }
  }

  return err
}

export const HttpError = createHttpErrorConstructor()
export const isHttpError = createIsHttpErrorFunction(HttpError)

statuses.codes.forEach(function forEachCode(code) {
  let CodeError
  const name = toIdentifier(statuses.message[code])

  switch(codeClass(code)) {
    case 400:
      CodeError = createClientErrorConstructor(HttpError, name, code)
      break
    case 500:
      CodeError = createServerErrorConstructor(HttpError, name, code)
      break
  }

  if(CodeError) {
    // export the constructor
    createError[code] = CodeError
    createError[name] = CodeError
  }
})

/**
 * Get the code class of a status code.
 */
function codeClass(status) {
  return Number(String(status).charAt(0) + '00')
}

/**
 * Create HTTP error abstract base class.
 */
function createHttpErrorConstructor() {
  function HttpError() {
    throw new TypeError('cannot construct abstract class')
  }

  inherits(HttpError, Error)

  return HttpError
}

/**
 * Create a constructor for a client error.
 */
function createClientErrorConstructor(HttpError, name, code) {
  const className = toClassName(name)

  function ClientError(message) {
    // create the error object
    const msg = message != null ? message : statuses.message[code]
    const err = new Error(msg)

    // capture a stack trace to the construction point
    Error.captureStackTrace(err, ClientError)

    // adjust the [[Prototype]]
    Object.setPrototypeOf(err, ClientError.prototype)

    // redefine the error message
    Object.defineProperty(err, 'message', {
      enumerable: true,
      configurable: true,
      value: msg,
      writable: true
    })

    // redefine the error name
    Object.defineProperty(err, 'name', {
      enumerable: false,
      configurable: true,
      value: className,
      writable: true
    })

    return err
  }

  inherits(ClientError, HttpError)
  nameFunc(ClientError, className)

  ClientError.prototype.status = code
  ClientError.prototype.statusCode = code
  ClientError.prototype.expose = true

  return ClientError
}

/**
 * Create function to test is a value is a HttpError.
 */
function createIsHttpErrorFunction(HttpError) {
  return function isHttpError(val) {
    if(!val || typeof val !== 'object') {
      return false
    }

    if(val instanceof HttpError) {
      return true
    }

    return val instanceof Error &&
      typeof val.expose === 'boolean' &&
      typeof val.statusCode === 'number' && val.status === val.statusCode
  }
}

/**
 * Create a constructor for a server error.
 */
function createServerErrorConstructor(HttpError, name, code) {
  const className = toClassName(name)

  function ServerError(message) {
    // create the error object
    const msg = message != null ? message : statuses.message[code]
    const err = new Error(msg)

    // capture a stack trace to the construction point
    Error.captureStackTrace(err, ServerError)

    // adjust the [[Prototype]]
    Object.setPrototypeOf(err, ServerError.prototype)

    // redefine the error message
    Object.defineProperty(err, 'message', {
      enumerable: true,
      configurable: true,
      value: msg,
      writable: true
    })

    // redefine the error name
    Object.defineProperty(err, 'name', {
      enumerable: false,
      configurable: true,
      value: className,
      writable: true
    })

    return err
  }

  inherits(ServerError, HttpError)
  nameFunc(ServerError, className)

  ServerError.prototype.status = code
  ServerError.prototype.statusCode = code
  ServerError.prototype.expose = false

  return ServerError
}

/**
 * Set the name of a function, if possible.
 */
function nameFunc(func, name) {
  const desc = Object.getOwnPropertyDescriptor(func, 'name')

  if(desc && desc.configurable) {
    desc.value = name
    Object.defineProperty(func, 'name', desc)
  }
}

/**
 * Get a class name from a name identifier.
 */
function toClassName(name) {
  return name.substr(-5) !== 'Error'
    ? name + 'Error'
    : name
}
